Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion and POC for using a React Context for the Security Rules page #198578

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

seanrathier
Copy link
Contributor

@seanrathier seanrathier commented Oct 31, 2024

Summary

Code speaks louder than words. 😉

I put together a real-world example using a complex page of what a refactor to using a React Context would look like so that we can discuss and put together an action plan (or no action) to identify and address changes for other pages.

Things I noticed

  1. We can do refactors like this over multiple PRs, we don't need to do this in one PR.
  2. Moving the business logic to the Context allowed me to understand easier how the page behaves during state change.
  3. I was able to fix bugs much easier since much of the logic is in the context.
  4. When I search for all references to a function or data object exported from the Context, I can see the places it is used. When we are prop drilling, we do not see where it is being used if it is passed down to sibling components.
  5. Fewer props were needed in components.
  6. The components are not implementing business logic, nor are they fetching data therefore they are acting more like pure components.
  7. There is some duplication between the usage of rulesPage, rulesQuery and page attributes. The page attribute exists in rulesPage and rulesQuery but seems to be used interchangeably in the components. This is a good example of what a Context could resolve for us. I've fixed this by removing the rulesQuery and rulesPage objects and passing mostly primitive types and arrays.

Things I could have done but thought it was enough for a demo.

  1. Create a Context test to test state changes and expected behaviours
  2. Create some UI tests using the Context, this will remove some complexity with mocks since we are creating the data
  3. Introduce a useReducer pattern to control state changes. However, this would have made this page complex

Issues to resolve

  1. Fetching all rules, I could have spent more time preventing all rules from being fetched on every state change, considered useRef but we might be able to set a prop in useQuery.
  2. Fix the paging issue
  3. The UnifiedDataTable is causing an infinite loop when it receives the same pagination and number of pages. The Unified table should be checking this before updating its state.
  4. Remove the dependency on useEffect in the Context.
  5. The loading status needs to be implemented

Checklist

Delete any items that do not apply to this PR.

@elasticmachine
Copy link
Contributor

🤖 Jobs for this PR can be triggered through checkboxes. 🚧

ℹ️ To trigger the CI, please tick the checkbox below 👇

  • Click to trigger kibana-pull-request for this PR!
  • Click to trigger kibana-deploy-project-from-pr for this PR!

if (sortOrder) {
setSortDirection(sortOrder.direction);
onSortChange(sortOrder.direction);
if (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to have exposed an endless state change loop here, therefore the checks

@seanrathier seanrathier added Team:Cloud Security Cloud Security team related discuss labels Nov 4, 2024
@seanrathier seanrathier self-assigned this Nov 6, 2024
@seanrathier seanrathier force-pushed the rules-state-reducer-pattern branch from d55f76d to ffc8807 Compare November 7, 2024 21:52
Copy link
Contributor

@albertoblaz albertoblaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, take my feedback with a grain of salt since I still lack of a lot of context 😛 (pun intended).

Overall, I think extracting state management and UI logic into this hooks helps to simplify RulesContainer, and reducing all the prop-drilling is a great effort 👍

I'm only concerned with parts of the state that used to be local and are now centralized here, which might cause additional re-renderings but I haven't done any testing. If you've tested it and works fine then ignore this :)

Comment on lines +116 to +119
setRuleNumber(
changedRuleNumbers?.selectedOptionKeys
? changedRuleNumbers?.selectedOptionKeys
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked how frequently these components re-render? Something I'm a little concerned with is the fact that we're replacing local state that only affected a single component, like this one with the rule number, and now we're putting it into context. Wouldn't the context hook, and the rest of siblings re-render as soon as this value is mutated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad you mentioned this, the other components do in fact need to re-render. Changing the rule filter needs to update the rules data fetched for the rules_table and rules_counters components.

If you pull the lastest main branch and turn on highlight update in the DevTools Profiler you will see many updates even outside of the Rules page.

A change like this is not ONLY about performance and re-renders, this is making the logic easier to understand and work with.

Screenshot 2024-11-08 at 12 57 58 PM

@seanrathier
Copy link
Contributor Author

@albertoblaz, thanks for the comments!

I'm only concerned with parts of the state that used to be local and are now centralized here, which might cause additional re-renderings but I haven't done any testing. If you've tested it and works fine then ignore this :)

I have not seen a difference in re-renders, but using a context such as this where the main business logic for the page is encapsulated in the Context shows me where we can act on performance enhancements.

One such example, we are fetching ALL the rules for a specific benchmark on every render (AWS, Kubernetes, etc), we can make some changes there by using a useRef to ensure that all the rules are only fetched once.

The Rules page is complex in its current state because we have business logic in components that are dependent on the props that are passed to it, additionally, we have the functionality to update the status of the rules in multiple places. When we need to make changes to the rules page, it is hard to follow the logic because it is spread across 4 component files and their inner components.

I like to subscribe to the notion of avoiding hasty performance enhancements, until it is a real problem to solve.

@maxcold
Copy link
Contributor

maxcold commented Nov 12, 2024

Thanks for putting up the POC!
I mostly had experience using context for small atomic pieces of state required for components spread in the component tree far away from each other. In your example, it feels like the context is used not in the way it was intended to be used. Here is why I have this feeling:

  • the context is huge, almost the whole state of the rules page is packed into one context
  • the consumer components are not spread in the component tree, they are all on the same level under one container
    I don't have specific arguments for why it might bite us if we go with this approach, except the unnecessary rerenders which has already been brought up and the fact that for every future component even if it relies on only one piece of the state, it will be hard not to use this huge context

@seanrathier
Copy link
Contributor Author

seanrathier commented Nov 13, 2024

  • the context is huge, almost the whole state of the rules page is packed into one context

@maxcold you are 100% right. This is a large context, however, this is mostly what the RulesContainer component is doing today with a few minor changes...

  • I destructured the rulesQuery and rulesPageData so the context now looks larger
    • We were duplicating the pageSize attribute in both those objects
    • Objects are not good for state change validation
    • We are currently using useMemo for rulesQuery and rulesPageData; however with objects, we need to do a deep comparison with useMemo
    • The destructured objects attributes are mostly arrays and primitive types

the consumer components are not spread in the component tree, they are all on the same level under one container
I don't have specific arguments for why it might bite us if we go with this approach, except the unnecessary rerenders

When you say rerender, are we talking about rendering a virtual DOM or the page? I do not see a difference in page rendering between this proposal and what is currently in use. This change proposal is more about the readability and maintainability of the page(s). When we hastily address performance before there is a performance problem, we are adding more code that JS needs to parse and execute, which may inhibit the performance of the page, the medicine is worse than the bite. ;)

What this POC raises are the places where we can make significant impacts whereas it is currently buried with prop drilling. One example is querying ALL the rules so that we can use them to build table filters and muted rule counts . The only data that can change as a result of user action is the muted rules count the other data, section and rule number list, can be refs that are not changed between renders because they don't change as a result of use interaction.

@maxcold
Copy link
Contributor

maxcold commented Nov 18, 2024

@seanrathier

This is a large context, however, this is mostly what the RulesContainer component is doing today with a few minor changes...

My main concern is that this example might not be a good one for the prop drill problem. You have almost all the components on the same level with some exceptions like SearchQuery

<RulesProvider>
        <RulesCounters />
        <EuiSpacer />
        <RulesTableHeader />
        <EuiSpacer />
        <RulesTable selectedRuleId={params.ruleId} onRuleClick={navToRuleFlyout} />
        <RuleFlyout onClose={navToRulePage} />
</RulesProvider>

It looks very clean, but for me it feels somehow off to share the logic and state like that through huge top level context.

When you say rerender, are we talking about rendering a virtual DOM or the page?

I'm talking about components rerendering when smth in the context changes even of these components do not rely on the piece that changes. For example RuleFlyout component right now re-renders whenever smth change in the context, eg. user types in the search. Yes, we have the early exit in this component, but this is a risk that I see - with a huge context it's hard to reason about component rerendering.

But I also understand that "smth feels off for me" and concerns about potential performance problems are NOT good arguments, so I wouldn't stand in the way of such refactoring if the team sees value in it

@albertoblaz
Copy link
Contributor

As I said before, I'm not the most suitable person to chime in on this discussion since I don't know the underlying implementation details.

@maxcold said:

I'm talking about components rerendering when smth in the context changes even of these components do not rely on the piece that changes. For example RuleFlyout component right now re-renders whenever smth change in the context, eg. user types in the search. Yes, we have the early exit in this component, but this is a risk that I see - with a huge context it's hard to reason about component rerendering.

This is something I'm a little concerned with, though. If a component re-renders because something unrelated to it changed in the context's state, then that sounds like a performance regression and we should avoid it. I honestly haven't tested this myself locally so please @seanrathier correct me if I misunderstood.

Also, on the one hand, I agree with @maxcold that, in this particular case, we might not need to move the state from the parent to a context since the component sub-tree is so simple. But there's not really a "right" answer here, it's a matter of preference.

On the other hand, I definitely see some value on keeping part of the changes like turning async callbacks into sync ones, or actually fixing the effect to re-run it when all its dependencies change.

Would be great if more people contribute to the discussion @elastic/kibana-cloud-security-posture. @seanrathier has had this PR open for 3 weeks and even though this has low-prio, we should make a decision at some point. Whether we ship this or not, it'd be great to provide some feedback and agree as a team on certain patterns/styles :)

@maxcold
Copy link
Contributor

maxcold commented Nov 20, 2024

Lol, as often happens I missed NOT where it mattered :)

But I also understand that "smth feels off for me" and concerns about potential performance problems are NOT good arguments, so I wouldn't stand in the way of such refactoring if the team sees value in it

@acorretti acorretti added this to the 8.18 milestone Nov 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss Team:Cloud Security Cloud Security team related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants